// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.

package satellitedb

import (
	"context"
	"strings"

	"github.com/zeebo/errs"

	"storj.io/common/uuid"
	"storj.io/storj/satellite/console"
	"storj.io/storj/satellite/satellitedb/dbx"
)

// ensures that projectMembers implements console.ProjectMembers.
var _ console.ProjectMembers = (*projectMembers)(nil)

// ProjectMembers exposes methods to manage ProjectMembers table in database.
type projectMembers struct {
	methods dbx.Methods
	db      *satelliteDB
}

// GetByMemberID is a method for querying project member from the database by memberID.
func (pm *projectMembers) GetByMemberID(ctx context.Context, memberID uuid.UUID) (_ []console.ProjectMember, err error) {
	defer mon.Task()(&ctx)(&err)
	projectMembersDbx, err := pm.methods.All_ProjectMember_By_MemberId(ctx, dbx.ProjectMember_MemberId(memberID[:]))
	if err != nil {
		return nil, err
	}

	return projectMembersFromDbxSlice(ctx, projectMembersDbx)
}

// GetByProjectID is a method for querying project members from the database by projectID, offset and limit.
func (pm *projectMembers) GetPagedByProjectID(ctx context.Context, projectID uuid.UUID, cursor console.ProjectMembersCursor) (_ *console.ProjectMembersPage, err error) {
	defer mon.Task()(&ctx)(&err)

	search := "%" + strings.ReplaceAll(cursor.Search, " ", "%") + "%"

	if cursor.Limit > 50 {
		cursor.Limit = 50
	}

	if cursor.Page == 0 {
		return nil, errs.New("page cannot be 0")
	}

	page := &console.ProjectMembersPage{
		Search:         cursor.Search,
		Limit:          cursor.Limit,
		Offset:         uint64((cursor.Page - 1) * cursor.Limit),
		Order:          cursor.Order,
		OrderDirection: cursor.OrderDirection,
	}

	countQuery := pm.db.Rebind(`
		SELECT COUNT(*)
		FROM project_members pm 
		INNER JOIN users u ON pm.member_id = u.id
		WHERE pm.project_id = ?
		AND ( u.email LIKE ? OR 
			  u.full_name LIKE ? OR
			  u.short_name LIKE ? 
		)`)

	countRow := pm.db.QueryRowContext(ctx,
		countQuery,
		projectID[:],
		search,
		search,
		search)

	err = countRow.Scan(&page.TotalCount)
	if err != nil {
		return nil, err
	}
	if page.TotalCount == 0 {
		return page, nil
	}
	if page.Offset > page.TotalCount-1 {
		return nil, errs.New("page is out of range")
	}
	// TODO: LIKE is case-sensitive postgres, however this should be case-insensitive and possibly allow typos
	reboundQuery := pm.db.Rebind(`
		SELECT pm.*
			FROM project_members pm
				INNER JOIN users u ON pm.member_id = u.id
				WHERE pm.project_id = ?
				AND ( u.email LIKE ? OR
					u.full_name LIKE ? OR
					u.short_name LIKE ? )
					ORDER BY ` + sanitizedOrderColumnName(cursor.Order) + `
					` + sanitizeOrderDirectionName(page.OrderDirection) + `	
					LIMIT ? OFFSET ?`)

	rows, err := pm.db.QueryContext(ctx,
		reboundQuery,
		projectID[:],
		search,
		search,
		search,
		page.Limit,
		page.Offset)

	defer func() {
		err = errs.Combine(err, rows.Close())
	}()

	if err != nil {
		return nil, err
	}

	var projectMembers []console.ProjectMember
	for rows.Next() {
		pm := console.ProjectMember{}

		err = rows.Scan(&pm.MemberID, &pm.ProjectID, &pm.CreatedAt)
		if err != nil {
			return nil, err
		}

		projectMembers = append(projectMembers, pm)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	page.ProjectMembers = projectMembers
	page.Order = cursor.Order

	page.PageCount = uint(page.TotalCount / uint64(cursor.Limit))
	if page.TotalCount%uint64(cursor.Limit) != 0 {
		page.PageCount++
	}

	page.CurrentPage = cursor.Page

	return page, err
}

// Insert is a method for inserting project member into the database.
func (pm *projectMembers) Insert(ctx context.Context, memberID, projectID uuid.UUID) (_ *console.ProjectMember, err error) {
	defer mon.Task()(&ctx)(&err)
	createdProjectMember, err := pm.methods.Create_ProjectMember(ctx,
		dbx.ProjectMember_MemberId(memberID[:]),
		dbx.ProjectMember_ProjectId(projectID[:]))
	if err != nil {
		return nil, err
	}

	return projectMemberFromDBX(ctx, createdProjectMember)
}

// Delete is a method for deleting project member by memberID and projectID from the database.
func (pm *projectMembers) Delete(ctx context.Context, memberID, projectID uuid.UUID) (err error) {
	defer mon.Task()(&ctx)(&err)
	_, err = pm.methods.Delete_ProjectMember_By_MemberId_And_ProjectId(
		ctx,
		dbx.ProjectMember_MemberId(memberID[:]),
		dbx.ProjectMember_ProjectId(projectID[:]),
	)

	return err
}

// projectMemberFromDBX is used for creating ProjectMember entity from autogenerated dbx.ProjectMember struct.
func projectMemberFromDBX(ctx context.Context, projectMember *dbx.ProjectMember) (_ *console.ProjectMember, err error) {
	defer mon.Task()(&ctx)(&err)
	if projectMember == nil {
		return nil, errs.New("projectMember parameter is nil")
	}

	memberID, err := uuid.FromBytes(projectMember.MemberId)
	if err != nil {
		return nil, err
	}

	projectID, err := uuid.FromBytes(projectMember.ProjectId)
	if err != nil {
		return nil, err
	}

	return &console.ProjectMember{
		MemberID:  memberID,
		ProjectID: projectID,
		CreatedAt: projectMember.CreatedAt,
	}, nil
}

// sanitizedOrderColumnName return valid order by column.
func sanitizedOrderColumnName(pmo console.ProjectMemberOrder) string {
	switch pmo {
	case 2:
		return "u.email"
	case 3:
		return "pm.created_at"
	default:
		return "u.full_name"
	}
}

func sanitizeOrderDirectionName(pmo console.OrderDirection) string {
	if pmo == 2 {
		return "DESC"
	}

	return "ASC"
}

// projectMembersFromDbxSlice is used for creating []ProjectMember entities from autogenerated []*dbx.ProjectMember struct.
func projectMembersFromDbxSlice(ctx context.Context, projectMembersDbx []*dbx.ProjectMember) (_ []console.ProjectMember, err error) {
	defer mon.Task()(&ctx)(&err)
	var projectMembers []console.ProjectMember
	var errors []error

	// Generating []dbo from []dbx and collecting all errors
	for _, projectMemberDbx := range projectMembersDbx {
		projectMember, err := projectMemberFromDBX(ctx, projectMemberDbx)
		if err != nil {
			errors = append(errors, err)
			continue
		}

		projectMembers = append(projectMembers, *projectMember)
	}

	return projectMembers, errs.Combine(errors...)
}
